import Ajv from 'ajv';
import type { Options, ErrorObject, AsyncValidateFunction } from 'ajv';
import type { Schema } from 'ajv/lib/types';
import type { JSONSchemaType } from 'ajv/lib/types/json-schema';
import type { JTDSchemaType, SomeJTDSchemaType } from 'ajv/lib/types/jtd-schema';
import type { JsonSchemaProperty } from './schema';
import { ValidationError } from './ValidationError';

export type AjvErrorKeyword =
  | 'additionalItems' | 'items'
  | 'additionalProperties'
  | 'anyOf'
  | 'const'
  | 'contains'
  | 'dependencies' | 'dependentRequired'
  | 'discriminator'
  | 'enum'
  | 'false schema'
  | 'format'
  | 'formatMaximum' | 'formatExclusiveMaximum'
  | 'formatMinimum' | 'formatExclusiveMinimum'
  | 'if'
  | 'maximum' | 'exclusiveMaximum'
  | 'maxItems'
  | 'maxLength'
  | 'maxProperties'
  | 'minimum' | 'exclusiveMinimum'
  | 'minItems'
  | 'minLength'
  | 'minProperties'
  | 'multipleOf'
  | 'not'
  | 'oneOf'
  | 'pattern'
  | 'patternRequired'
  | 'propertyNames'
  | 'required'
  | 'type'
  | 'unevaluatedItems'
  | 'unevaluatedProperties'
  | 'uniqueItems'

// maybe not in use
export type AjvDiscriminatorError = 'tag' | 'mapping'

// align to ajv-i18n
export type AjvLocalizeCallback = (errors?: null | ErrorObject[]) => void;

export type AjvLocalizeErrorHandleCallback = (error: ErrorObject) => void;

export type AjvLocalizeErrorItem = [AjvErrorKeyword, AjvLocalizeErrorHandleCallback]

export type AjvLocalizeErrorMap = Record<AjvErrorKeyword, AjvLocalizeErrorHandleCallback>

export type AjvLocalizeDeclaration = AjvLocalizeCallback | AjvLocalizeErrorItem | AjvLocalizeErrorMap

export type AjvLocalizeHandleMap = Partial<Record<AjvErrorKeyword, AjvLocalizeErrorHandleCallback[]>>;

export type AjvSchema<T = unknown> =
  | Schema | JSONSchemaType<T> | JTDSchemaType<T> | SomeJTDSchemaType
  | JsonSchemaProperty

export type AjvValidateResult = {
  ajv: Ajv,
  valid: boolean,
  errors?: ErrorObject[],
}

export type AjvUseCallback = (ajv: Ajv, create: CreateAjv) => void;

export type CreateAjv = {
  (options?: Options): Ajv,
  use(fn: AjvUseCallback): CreateAjv,
  useLocalize(fn: AjvLocalizeCallback): CreateAjv,
  setOptions(options: Options): CreateAjv,
  localize(errors?: null | ErrorObject[]): void,
  validate<T = unknown>(data: unknown, schema: AjvSchema<T>, throwError?: boolean): AjvValidateResult,
  validateAsync<T = unknown>(data: unknown, schema: AjvSchema<T>, throwError?: boolean): Promise<AjvValidateResult>,
  isValidationError(err: unknown): err is ValidationError,
};

export function createAjv(): CreateAjv {
  const localizes: AjvLocalizeCallback[] = [];
  const callbacks: AjvUseCallback[] = [];
  const defaultOptions: Options = {};

  const create: CreateAjv = (options: Options = {}): Ajv => {
    const ajv = new Ajv({ ...defaultOptions, ...options });
    if (callbacks.length) {
      callbacks.forEach((fn) => fn(ajv, create));
    }
    return ajv;
  };

  create.setOptions = (options: Options): CreateAjv => {
    Object.assign(defaultOptions, options);
    return create;
  };

  create.use = (fn: AjvUseCallback): CreateAjv => {
    if (!callbacks.includes(fn)) {
      callbacks.push(fn);
    }
    return create;
  };

  create.useLocalize = (fn: AjvLocalizeCallback): CreateAjv => {
    if (!localizes.includes(fn)) {
      localizes.push(fn);
    }
    return create;
  };

  const localize = create.localize = (errors?: null | ErrorObject[]): void => {
    if (errors != null && errors.length && localizes.length) {
      localizes.forEach(fn => fn(errors));
    }
  };

  create.validate = <T = unknown>(data: unknown, schema: AjvSchema<T>, throwError?: boolean): AjvValidateResult => {
    const ajv = create();
    const v = ajv.compile<T>(schema);
    let valid = v(data);
    let errors: ErrorObject[] | undefined;
    if (v.errors && v.errors.length) {
      valid = false;
      localize(v.errors);
      errors = v.errors;
      if (throwError) {
        throw new ValidationError(ajv, errors);
      }
    }
    return { valid, ajv, errors };
  };

  create.validateAsync = async <T = unknown>(data: unknown, schema: AjvSchema<T>, throwError?: boolean): Promise<AjvValidateResult> => {
    const ajv = create();
    const v = ajv.compile<T>(schema) as AsyncValidateFunction<T>;
    await v(data);
    let valid = true;
    let errors: ErrorObject[] | undefined;
    if (v.errors && v.errors.length) {
      valid = false;
      localize(v.errors);
      errors = v.errors;
      if (throwError) {
        throw new ValidationError(ajv, errors);
      }
    }
    return { valid, ajv, errors };
  };

  create.isValidationError = (err: unknown): err is ValidationError =>
    err instanceof ValidationError;

  return create;
}

export const ajv = createAjv();
